{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "afe5874f",
   "metadata": {},
   "source": [
    "## 人机协同\n",
    "\n",
    "Langgraph 通过审批、编辑和输入三种常见的用户交互模式，增强 Agent 的能力。这个模式被称为“人机协同”（human in the loop , HITL）\n",
    "\n",
    "审批模式，表示可以中断 Agent，将当前的状态交给用户，并允许用户接受或拒绝 Agent 的决策。\n",
    "\n",
    "编辑模式，表示可以中断 Agent，让用户直接编辑状态，然后让 Agent 继续执行。\n",
    "\n",
    "输入模式，表示可以中断 Agent，可以专门创建一个图节点来收集人类输入，并将该输入直接传递给 Agent。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "dcd36ca4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " LangGraph 人机协同（HITL）演示\n",
      "============================================================\n",
      "\n",
      " 场景1：小额退款（300元）- 自动处理\n",
      "\n",
      " 开始处理退款申请...\n",
      "==================================================\n",
      " 收到退款申请：300.0 元\n",
      " AI评估结果： 300.0 元退款自动批准\n",
      "  退款 300.0 元已批准并处理\n",
      "==================================================\n",
      " 退款流程完成\n",
      "\n",
      "============================================================\n",
      "\n",
      " 场景2：大额退款（800元）- 需要人工审批\n",
      " 这里会真正暂停等待你的输入！\n",
      "\n",
      " 开始处理退款申请...\n",
      "==================================================\n",
      " 收到退款申请：800.0 元\n",
      " AI评估结果： 800.0 元退款需要人工审核\n",
      " 等待人工审批 800.0 元退款...\n",
      " 人工决策：批准\n",
      "  退款 800.0 元已批准并处理\n",
      "==================================================\n",
      " 退款流程完成\n"
     ]
    }
   ],
   "source": [
    "# LangGraph 人机协同（HITL）演示 \n",
    "\n",
    "from typing import Annotated, Sequence\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph, START, END, add_messages\n",
    "from langchain_core.messages import HumanMessage, AIMessage\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "# 状态定义\n",
    "class RefundState(TypedDict):\n",
    "    messages: Annotated[Sequence, add_messages]\n",
    "    refund_amount: float\n",
    "    needs_approval: bool\n",
    "\n",
    "# 节点函数\n",
    "def receive_request(state: RefundState):\n",
    "    \"\"\"接收退款请求\"\"\"\n",
    "    last_msg = state[\"messages\"][-1].content.strip()\n",
    "    try:\n",
    "        amount = float(last_msg)\n",
    "    except:\n",
    "        amount = 0.0\n",
    "    \n",
    "    print(f\" 收到退款申请：{amount} 元\")\n",
    "    return {\n",
    "        \"refund_amount\": amount,\n",
    "        \"needs_approval\": False\n",
    "    }\n",
    "\n",
    "def ai_evaluate(state: RefundState):\n",
    "    \"\"\"AI评估退款请求\"\"\"\n",
    "    amount = state[\"refund_amount\"]\n",
    "    \n",
    "    if amount <= 0:\n",
    "        reply = \" 退款金额无效\"\n",
    "        needs_approval = False\n",
    "    elif amount <= 500:\n",
    "        reply = f\" {amount} 元退款自动批准\"\n",
    "        needs_approval = False\n",
    "    else:\n",
    "        reply = f\" {amount} 元退款需要人工审核\"\n",
    "        needs_approval = True\n",
    "    \n",
    "    print(f\" AI评估结果：{reply}\")\n",
    "    \n",
    "    return {\n",
    "        \"messages\": [AIMessage(content=reply)],\n",
    "        \"needs_approval\": needs_approval\n",
    "    }\n",
    "\n",
    "def human_approval(state: RefundState):\n",
    "    \"\"\"人工审批节点 - 这里会暂停等待输入\"\"\"\n",
    "    amount = state[\"refund_amount\"]\n",
    "    \n",
    "    print(f\" 等待人工审批 {amount} 元退款...\")\n",
    "    \n",
    "    # 获取人工输入（通过 input() 真正暂停）\n",
    "    while True:\n",
    "        try:\n",
    "            decision = input(f\"\\n请审批 {amount} 元退款申请 (输入 '是' 批准, '否' 拒绝): \").strip()\n",
    "            if decision in ['是', '否', 'yes', 'no', 'y', 'n']:\n",
    "                break\n",
    "            else:\n",
    "                print(\"请输入 '是' 或 '否'\")\n",
    "        except KeyboardInterrupt:\n",
    "            print(\"\\n 审批被取消\")\n",
    "            decision = '否'\n",
    "            break\n",
    "    \n",
    "    approved = decision in ['是', 'yes', 'y']\n",
    "    result = \"批准\" if approved else \"拒绝\"\n",
    "    \n",
    "    print(f\" 人工决策：{result}\")\n",
    "    \n",
    "    return {\n",
    "        \"messages\": [HumanMessage(content=f\"[人工审批] {result}\")]\n",
    "    }\n",
    "\n",
    "def finalize_refund(state: RefundState):\n",
    "    \"\"\"最终处理退款\"\"\"\n",
    "    amount = state[\"refund_amount\"]\n",
    "    \n",
    "    # 检查最后一条消息是否包含审批结果\n",
    "    last_msg = state[\"messages\"][-1].content if state[\"messages\"] else \"\"\n",
    "    \n",
    "    if \"批准\" in last_msg:\n",
    "        reply = f\" 退款 {amount} 元已批准并处理\"\n",
    "        print(f\" {reply}\")\n",
    "    elif \"拒绝\" in last_msg:\n",
    "        reply = f\" 退款 {amount} 元申请被拒绝\"\n",
    "        print(f\" {reply}\")\n",
    "    elif not state.get(\"needs_approval\", True):\n",
    "        reply = f\" 退款 {amount} 元自动处理完成\"\n",
    "        print(f\" {reply}\")\n",
    "    else:\n",
    "        reply = f\" 退款 {amount} 元状态未知\"\n",
    "        print(f\" {reply}\")\n",
    "    \n",
    "    return {\n",
    "        \"messages\": [AIMessage(content=reply)]\n",
    "    }\n",
    "\n",
    "# 路由函数\n",
    "def should_get_approval(state: RefundState) -> str:\n",
    "    \"\"\"决定是否需要人工审批\"\"\"\n",
    "    if state.get(\"needs_approval\", False):\n",
    "        return \"human_approval\"\n",
    "    else:\n",
    "        return \"finalize_refund\"\n",
    "\n",
    "# 构建图\n",
    "graph = StateGraph(RefundState)\n",
    "\n",
    "# 添加节点\n",
    "graph.add_node(\"receive_request\", receive_request)\n",
    "graph.add_node(\"ai_evaluate\", ai_evaluate)\n",
    "graph.add_node(\"human_approval\", human_approval)\n",
    "graph.add_node(\"finalize_refund\", finalize_refund)\n",
    "\n",
    "# 添加边\n",
    "graph.add_edge(START, \"receive_request\")\n",
    "graph.add_edge(\"receive_request\", \"ai_evaluate\")\n",
    "\n",
    "# 条件边：根据是否需要审批来路由\n",
    "graph.add_conditional_edges(\n",
    "    \"ai_evaluate\",\n",
    "    should_get_approval,\n",
    "    {\n",
    "        \"human_approval\": \"human_approval\",\n",
    "        \"finalize_refund\": \"finalize_refund\"\n",
    "    }\n",
    ")\n",
    "\n",
    "graph.add_edge(\"human_approval\", \"finalize_refund\")\n",
    "graph.add_edge(\"finalize_refund\", END)\n",
    "\n",
    "# 编译图\n",
    "checkpointer = MemorySaver()\n",
    "app = graph.compile(checkpointer=checkpointer)\n",
    "\n",
    "# 主要函数\n",
    "def process_refund(amount: str, thread_id: str = \"default\"):\n",
    "    \"\"\"处理退款申请\"\"\"\n",
    "    print(f\"\\n 开始处理退款申请...\")\n",
    "    print(\"=\"*50)\n",
    "    \n",
    "    config = {\"configurable\": {\"thread_id\": thread_id}}\n",
    "    \n",
    "    # 运行工作流\n",
    "    result = app.invoke({\n",
    "        \"messages\": [HumanMessage(content=amount)]\n",
    "    }, config=config)\n",
    "    \n",
    "    print(\"=\"*50)\n",
    "    print(\" 退款流程完成\")\n",
    "    \n",
    "    return result\n",
    "\n",
    "# 演示函数\n",
    "def demo():\n",
    "    \"\"\"演示不同场景\"\"\"\n",
    "    print(\" LangGraph 人机协同（HITL）演示\")\n",
    "    print(\"=\"*60)\n",
    "    \n",
    "    # 场景1：小额退款（自动处理）\n",
    "    print(\"\\n 场景1：小额退款（300元）- 自动处理\")\n",
    "    process_refund(\"300\", \"user1\")\n",
    "    \n",
    "    print(\"\\n\" + \"=\"*60)\n",
    "    \n",
    "    # 场景2：大额退款（需要人工审批）\n",
    "    print(\"\\n 场景2：大额退款（800元）- 需要人工审批\")\n",
    "    print(\" 这里会真正暂停等待你的输入！\")\n",
    "    process_refund(\"800\", \"user2\")\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    demo()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "MLOps",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
